Vegetation Condition • Drought & Stress Monitoring

VCI – Vegetation Condition Index

The Vegetation Condition Index (VCI) expresses how “healthy” or “stressed” vegetation is at a given time compared to its own historical minimum and maximum NDVI values. It is widely used for drought monitoring, crop stress detection, and early warning systems.

1. What is VCI?

VCI is a normalized index derived from NDVI, indicating the relative position of current NDVI values between historical (multi-year) minimum and maximum for the same pixel. Low VCI values indicate vegetation under stress (e.g. drought), while high VCI values indicate favorable conditions.

Mathematical Definition

VCI is usually defined as:

VCI = (NDVIcurrent − NDVImin) / (NDVImax − NDVImin) Dimensionless (0 to 1, often scaled 0–100)

Where:

  • NDVIcurrent: Current or recent NDVI (e.g. last dekad or month)
  • NDVImin: Historical minimum NDVI for each pixel over a reference period
  • NDVImax: Historical maximum NDVI for each pixel over the same period

Typical Interpretation (scaled 0–100)

VCI Range Interpretation
0 – 20 Severe stress / extreme drought conditions
20 – 40 Moderate to high vegetation stress
40 – 60 Near normal vegetation condition
60 – 80 Good growing conditions, healthy vegetation
80 – 100 Very favorable conditions / exceptionally lush vegetation

Use: drought early warning • crop monitoring • seasonal assessments

2. Data & NDVI Time Series for VCI

Common Sensors & NDVI Source

  • Sentinel-2 (ESA) – 10 m
    • Red: B4 (~665 nm)
    • NIR: B8 (~842 nm)
    • NDVI derived as (NIR − Red) / (NIR + Red)
  • Landsat 8/9 OLI – 30 m
    • Red: B4
    • NIR: B5
    • NDVI derived similarly

Reference Period

VCI requires a multi-year NDVI time series to estimate NDVI_min and NDVI_max for each pixel. Typical reference windows:

  • At least 3–5 years of NDVI data
  • Consistent seasonal window (e.g. growing season months)

Good Practice

  • Use surface reflectance products with atmospheric correction.
  • Filter out cloudy scenes using cloud percentage and masks.
  • Compute NDVI composites (e.g. median) for specific periods (dekad, month, season).
  • Ensure the historical min/max are computed using comparable seasonal windows.

3. Google Earth Engine Code – VCI from Sentinel-2 NDVI

Steps: open code.earthengine.google.com → New Script → paste the code → draw your AOI as geometry on the map → click Run. The script builds a multi-year NDVI climatology, then computes VCI for a recent period and exports it as GeoTIFF to Google Drive.

// VCI (Vegetation Condition Index) from Sentinel-2 NDVI
// -----------------------------------------------------
// This script:
// 1) Uses Sentinel-2 SR over a multi-year period to compute per-pixel NDVI_min & NDVI_max.
// 2) Computes a recent NDVI composite (target period).
// 3) Derives VCI = (NDVI_current - NDVI_min) / (NDVI_max - NDVI_min).
// 4) Displays and exports VCI as GeoTIFF.

// -------------------------------------------------------
// 1. Define Area of Interest (AOI)
// -------------------------------------------------------
var roi = geometry;  // Make sure a 'geometry' object exists in the left panel

// Center the map on the AOI
Map.centerObject(roi, 8);

// -------------------------------------------------------
// 2. Define time ranges
// -------------------------------------------------------
// Reference period (for NDVI_min and NDVI_max climatology)
// You can adjust these years based on your study
var refStart = '2018-01-01';
var refEnd   = '2023-12-31';

// Target (current) period for which you want VCI
var targetStart = '2023-06-01';
var targetEnd   = '2023-08-31';  // example: a summer season window

// -------------------------------------------------------
// 3. Function to mask clouds (Sentinel-2 SR)
// -------------------------------------------------------
function maskS2clouds(image) {
  var scl = image.select('SCL');
  // Keep vegetation, bare soil, water etc. and remove clouds/shadows
  var mask = scl.eq(4)   // vegetation
    .or(scl.eq(5))       // not vegetated
    .or(scl.eq(6))       // water
    .or(scl.eq(7))       // unclassified
    .or(scl.eq(11));     // snow/ice (optional)
  return image.updateMask(mask);
}

// -------------------------------------------------------
// 4. Build NDVI image collection for the reference period
// -------------------------------------------------------
var s2_ref = ee.ImageCollection('COPERNICUS/S2_SR')
  .filterBounds(roi)
  .filterDate(refStart, refEnd)
  .filter(ee.Filter.lt('CLOUDY_PIXEL_PERCENTAGE', 40))
  .map(maskS2clouds)
  .map(function (img) {
    var ndvi = img.normalizedDifference(['B8', 'B4']).rename('NDVI');
    return ndvi.copyProperties(img, img.propertyNames());
  });

// Compute per-pixel NDVI_min and NDVI_max over the reference period
var ndviMin = s2_ref.reduce(ee.Reducer.min()).rename('NDVI_min');
var ndviMax = s2_ref.reduce(ee.Reducer.max()).rename('NDVI_max');

// -------------------------------------------------------
// 5. Compute NDVI for the target (current) period
// -------------------------------------------------------
var s2_target = ee.ImageCollection('COPERNICUS/S2_SR')
  .filterBounds(roi)
  .filterDate(targetStart, targetEnd)
  .filter(ee.Filter.lt('CLOUDY_PIXEL_PERCENTAGE', 40))
  .map(maskS2clouds)
  .map(function (img) {
    var ndvi = img.normalizedDifference(['B8', 'B4']).rename('NDVI');
    return ndvi.copyProperties(img, img.propertyNames());
  });

// Use median NDVI over the target period
var ndviCurrent = s2_target.median().rename('NDVI_current');

// -------------------------------------------------------
// 6. Compute VCI
// -------------------------------------------------------
// Avoid division by zero: add a small epsilon to denominator
var epsilon = 1e-6;

var vci = ndviCurrent
  .subtract(ndviMin)
  .divide(ndviMax.subtract(ndviMin).add(epsilon))
  .rename('VCI');

// Optionally scale to 0–100
var vciScaled = vci.multiply(100).rename('VCI_0_100');

// -------------------------------------------------------
// 7. Visualization
// -------------------------------------------------------
var vciVis = {
  min: 0,
  max: 100,
  palette: [
    '#4c0000', // very low VCI (severe stress)
    '#b30000',
    '#ff4d4d',
    '#ffff66',
    '#66ff66',
    '#009933'  // very high VCI (lush vegetation)
  ]
};

Map.addLayer(vciScaled.clip(roi), vciVis, 'VCI (0–100)', true);

// Also show a true color composite for context
var rgb = ee.ImageCollection('COPERNICUS/S2_SR')
  .filterBounds(roi)
  .filterDate(targetStart, targetEnd)
  .filter(ee.Filter.lt('CLOUDY_PIXEL_PERCENTAGE', 40))
  .map(maskS2clouds)
  .select(['B4', 'B3', 'B2'])  // RGB
  .median()
  .clip(roi);

Map.addLayer(rgb, {min: 0, max: 3000}, 'True Color (RGB)', false);

// -------------------------------------------------------
// 8. Export VCI to Google Drive
// -------------------------------------------------------
Export.image.toDrive({
  image: vciScaled.clip(roi),
  description: 'VCI_Export',
  fileNamePrefix: 'VCI_0_100',
  folder: 'GEE_Exports',
  scale: 20,         // adjust based on resolution (10–30 m)
  region: roi,
  maxPixels: 1e13
});

// End of VCI script
```0